﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.DirectoryServices;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
using Microsoft.Interop.Security.AzRoles;

namespace AzManPermissions
{
  /// <summary>
  /// Helper methods and properties for directly connecting and handling an AzMan store and some AD searches.
  /// </summary>
  /// <remarks>
  /// It can be used on directly connected AzMan stores. If you need this functionality on a remote store, you have to
  /// create a service with the functions needed.
  /// </remarks>
  public static class AzManOperationHelper
  {
    #region Constructors

    /// <summary>
    /// Static constructor to read default connection information from the application's configuration.
    /// </summary>
    static AzManOperationHelper()
    {
      NameValueCollection appSettings = ConfigurationManager.AppSettings;
      if (appSettings != null)
      {
        string connection = appSettings["AzManConnectionString"];
        if (!string.IsNullOrWhiteSpace(connection))
          ConnectionString = connection;
        string application = appSettings["AzManApplicationName"];
        if (!string.IsNullOrWhiteSpace(application))
          ApplicationName = application;
        else
          SetApplicationNameFromApp();
      }
      else
        SetApplicationNameFromApp();
    }

    #endregion Constructors

    #region Properties

    private static string connectionString;

    /// <summary>
    /// Connection string to the AzMan policy store.
    /// </summary>
    public static string ConnectionString
    {
      get { return connectionString; }
      set
      {
        connectionString = value;
        isNewAppRequired = true;
      }
    }

    private static string applicationName;

    /// <summary>
    /// Name of the application in the AzMan policy store.
    /// </summary>
    public static string ApplicationName
    {
      get { return applicationName; }
      set
      {
        applicationName = value;
        isNewAppRequired = true;
      }
    }

    #endregion Properties

    #region Methods

    /// <summary>
    /// Updates the cache of objects and object attributes to match the underlying AzMan policy store.
    /// </summary>
    public static void UpdateCache()
    {
      azManStore.UpdateCache();
    }

    /// <summary>
    /// Retrieves the account names of accounts that belong to the specified role.
    /// </summary>
    /// <param name="roleName">Name of the role.</param>
    /// <returns>An array of strings containing each member's (Windows user or group) name that is assigned to the role.</returns>
    /// <exception cref="T:System.ArgumentException">The <paramref name="roleName" /> parameter is null.</exception>
    /// <exception cref="T:System.Runtime.InteropServices.COMException">The <paramref name="roleName" /> parameter is not a valid role in the AzMan application.</exception>
    public static string[] MemberNamesInRole(string roleName)
    {
      IAzRole role = AzManApp.OpenRole(roleName);
      string[] result = new string[((object[])role.MembersName).Length];
      ((object[])role.MembersName).CopyTo(result, 0);
      return result;
    }

    /// <summary>
    /// Retrieves the security identifiers (SIDs), in text form, of Windows accounts that belong to the specified role.
    /// </summary>
    /// <param name="roleName">Name of the role.</param>
    /// <returns>An array of strings containing each member's (Windows user or group) SID that is assigned to the role.</returns>
    /// <exception cref="T:System.ArgumentException">The <paramref name="roleName" /> parameter is null.</exception>
    /// <exception cref="T:System.Runtime.InteropServices.COMException">The <paramref name="roleName" /> parameter is not a valid role in the AzMan application.</exception>
    public static string[] MembersInRole(string roleName)
    {
      IAzRole role = AzManApp.OpenRole(roleName);
      string[] result = new string[((object[])role.Members).Length];
      ((object[])role.Members).CopyTo(result, 0);
      return result;
    }

    /// <summary>
    /// Retrieves information about all Windows accounts that belong to the specified role.
    /// </summary>
    /// <remarks>
    /// It searches recursively in AzMan and AD for users who belong to the specified role.
    /// </remarks>
    /// <param name="roleName">Name of the role.</param>
    /// <returns>Detailed information on each user that currently belong to the specified role.</returns>
    /// <exception cref="T:System.ArgumentException">The <paramref name="roleName" /> parameter is null.</exception>
    /// <exception cref="T:System.Runtime.InteropServices.COMException">The <paramref name="roleName" /> parameter is not a valid role in the AzMan application.</exception>
    public static MemberSearchResult[] AllMembersInRole(string roleName)
    {
      return AllMembersInRole(roleName, (string[])null);
    }

    /// <summary>
    /// Retrieves information about all Windows accounts that belong to the specified role.
    /// </summary>
    /// <remarks>
    /// It searches recursively in AzMan and AD for users who belong to the specified role.
    /// </remarks>
    /// <param name="roleName">Name of the role.</param>
    /// <param name="additionalProperty">An additional AD property name to collect for each account in the result.</param>
    /// <returns>Detailed information on each user that currently belong to the specified role, including the additional property requested.</returns>
    /// <exception cref="T:System.ArgumentException">The <paramref name="roleName" /> parameter is null.</exception>
    /// <exception cref="T:System.Runtime.InteropServices.COMException">The <paramref name="roleName" /> parameter is not a valid role in the AzMan application.</exception>
    public static MemberSearchResult[] AllMembersInRole(string roleName, string additionalProperty)
    {
      string[] properties = new string[1];
      properties[0] = additionalProperty;
      return AllMembersInRole(roleName, properties);
    }

    /// <summary>
    /// Retrieves information about all Windows accounts that belong to the specified role.
    /// </summary>
    /// <remarks>
    /// It searches recursively in AzMan and AD for users who belong to the specified role.
    /// </remarks>
    /// <param name="roleName">Name of the role.</param>
    /// <param name="additionalProperties">Additional AD property names to collect for each account in the result.</param>
    /// <returns>Detailed information on each user that currently belong to the specified role, including the additional properties requested.</returns>
    /// <exception cref="T:System.ArgumentException">The <paramref name="roleName" /> parameter is null.</exception>
    /// <exception cref="T:System.Runtime.InteropServices.COMException">The <paramref name="roleName" /> parameter is not a valid role in the AzMan application.</exception>
    public static MemberSearchResult[] AllMembersInRole(string roleName, string[] additionalProperties)
    {
      IAzRole role = AzManApp.OpenRole(roleName);
      List<MemberSearchResult> members = new List<MemberSearchResult>();
      List<MemberSearchResult> nonmembers = new List<MemberSearchResult>();
      using (DirectorySearcher searcher = new DirectorySearcher { SearchScope = SearchScope.Subtree })
      {
        foreach (string commonName in MemberSearchResult.commonPropertyNames)
        {
          searcher.PropertiesToLoad.Add(commonName);
        }
        if (additionalProperties != null)
        {
          foreach (string property in additionalProperties)
          {
            searcher.PropertiesToLoad.Add(property);
          }
        }
        ProcessMembers((object[])role.Members, searcher, members);
        ProcessAppMembers((object[])role.AppMembers, searcher, members, nonmembers);
      }
      return members.Except(nonmembers).ToArray();
    }

    /// <summary>
    /// Retrieves the application groups that belong to the spedcified role.
    /// </summary>
    /// <param name="roleName">Name of the role.</param>
    /// <returns>An array of strings containing each application group that belongs to the role.</returns>
    /// <exception cref="T:System.ArgumentException">The <paramref name="roleName" /> parameter is null.</exception>
    /// <exception cref="T:System.Runtime.InteropServices.COMException">The <paramref name="roleName" /> parameter is not a valid role in the AzMan application.</exception>
    public static string[] AppMembersInRole(string roleName)
    {
      IAzRole role = AzManApp.OpenRole(roleName);
      string[] result = new string[((object[])role.AppMembers).Length];
      ((object[])role.AppMembers).CopyTo(result, 0);
      return result;
    }

    #endregion Methods

    #region Internal and private helper methods and properties

    //true if opening a new AzMan application is required
    private static bool isNewAppRequired = true;

    //the AzMan store
    private readonly static IAzAuthorizationStore azManStore = new AzAuthorizationStore();

    /// <summary>
    /// If ApplicationName is not supplied in the configuration, it tries to get it from theentry assembly.
    /// </summary>
    private static void SetApplicationNameFromApp()
    {
      Assembly entryAssembly = Assembly.GetEntryAssembly();
      if (entryAssembly != null)
        ApplicationName = entryAssembly.GetName().Name;
      else
        throw new ApplicationException(AzManPermissions.Properties.Resources.NoApplicationNameSupplied);
    }

    private static IAzApplication azManApp;

    /// <summary>
    /// Gets the actual AzMan application.
    /// </summary>
    internal static IAzApplication AzManApp
    {
      get
      {
        if (azManApp == null || isNewAppRequired)
        {
          azManStore.Initialize(0, ConnectionString, null);
          azManApp = azManStore.OpenApplication(ApplicationName, null);
          isNewAppRequired = false;
        }
        return azManApp;
      }
    }

    /// <summary>
    /// Process all application groups in the specified application group name array.
    /// </summary>
    /// <param name="memberNames">Array of application group names.</param>
    /// <param name="searcher">The DirectorySearcher used to manage AD searches.</param>
    /// <param name="members">Collection of members.</param>
    /// <param name="nonmembers">Collection of non members.</param>
    private static void ProcessAppMembers(object[] memberNames, DirectorySearcher searcher, List<MemberSearchResult> members, List<MemberSearchResult> nonmembers)
    {
      foreach (string appmember in memberNames)
      {
        ProcessAppMember(appmember, searcher, members, nonmembers);
      }
    }

    /// <summary>
    /// Process all members in the specified member name array.
    /// </summary>
    /// <param name="memberNames">Array of member names.</param>
    /// <param name="searcher">The DirectorySearcher used to manage AD searches.</param>
    /// <param name="members">Collection of members.</param>
    private static void ProcessMembers(object[] memberNames, DirectorySearcher searcher, List<MemberSearchResult> members)
    {
      foreach (string member in memberNames)
      {
        SecurityIdentifier membersid = new SecurityIdentifier(member);
        ProcessSID(membersid, searcher, members);
      }
    }

    /// <summary>
    /// Process one application group member.
    /// </summary>
    /// <param name="appmember">Name of the application group.</param>
    /// <param name="searcher">The DirectorySearcher used to manage AD searches.</param>
    /// <param name="members">Collection of members.</param>
    /// <param name="nonmembers">Collection of non members.</param>
    private static void ProcessAppMember(string appmember, DirectorySearcher searcher, List<MemberSearchResult> members, List<MemberSearchResult> nonmembers)
    {
      IAzApplicationGroup appgroup = AzManApp.OpenApplicationGroup(appmember);
      ProcessAppMembers((object[])appgroup.AppMembers, searcher, members, nonmembers);
      ProcessAppMembers((object[])appgroup.AppNonMembers, searcher, nonmembers, members);
      ProcessMembers((object[])appgroup.Members, searcher, members);
      ProcessMembers((object[])appgroup.NonMembers, searcher, nonmembers);
    }

    /// <summary>
    /// Process one AD member (SID).
    /// </summary>
    /// <param name="SID">SID of the AD member.</param>
    /// <param name="searcher">The DirectorySearcher used to manage AD searches.</param>
    /// <param name="members">Collection of members.</param>
    private static void ProcessSID(SecurityIdentifier SID, DirectorySearcher searcher, List<MemberSearchResult> members)
    {
      MemberSearchResult sr = new MemberSearchResult(SID);
      if (members.Contains(sr))
        return;
      searcher.Filter = String.Format("(objectSid={0})", SID);
      SearchResult entry = searcher.FindOne();
      ProcessSearchResult(entry, searcher, members);
    }

    /// <summary>
    /// Process one element of an AD search.
    /// </summary>
    /// <param name="entry">A result item from an AD search.</param>
    /// <param name="searcher">The DirectorySearcher used to manage AD searches.</param>
    /// <param name="members">Collection of members.</param>
    private static void ProcessSearchResult(SearchResult entry, DirectorySearcher searcher, List<MemberSearchResult> members)
    {
      if (entry.Properties["objectClass"].Contains("group"))
        ProcessGroup(entry, searcher, members);
      else
        if (entry.Properties["objectClass"].Contains("user"))
        {
          SecurityIdentifier sid = new SecurityIdentifier((byte[])entry.Properties["objectSid"][0], 0);
          MemberSearchResult searchresult = new MemberSearchResult(sid);
          if (members.Contains(searchresult))
            return;
          else
          {
            searchresult.DistinguishedName = (string)entry.Properties["distinguishedName"][0];
            searchresult.AccountName = (string)entry.Properties["sAMAccountName"][0];
            searchresult.Properties = entry.Properties;
            members.Add(searchresult);
          }
        }
        else
          throw new ApplicationException(string.Format(Properties.Resources.UnknownObjectClass, entry.Properties["distinguishedName"]));
    }

    /// <summary>
    /// Process an AD group from a search.
    /// </summary>
    /// <param name="entry">A (group) result item from an AD search.</param>
    /// <param name="searcher">The DirectorySearcher used to manage AD searches.</param>
    /// <param name="members">Collection of members.</param>
    private static void ProcessGroup(SearchResult entry, DirectorySearcher searcher, List<MemberSearchResult> members)
    {
      searcher.Filter = String.Format("(memberOf={0})", entry.Properties["distinguishedName"][0]);
      using (SearchResultCollection groupMembers = searcher.FindAll())
      {
        foreach (SearchResult groupMember in groupMembers)
        {
          ProcessSearchResult(groupMember, searcher, members);
        }
      }
    }

    #endregion Internal and private helper methods and properties
  }
}